package parser

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"io/fs"
	"log"
	"net/http"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"gitee.com/go-errors/errors"
	"gitee.com/go-wena/wena/internal/mod"
	"github.com/segmentio/go-snakecase"
)

// Import 导入 <alias> "<package>"
type Import struct {
	Name  string `json:"name"`  //别名
	Path  string `json:"path"`  //包名
	Alias bool   `json:"alias"` //是否别名
}

func (imp *Import) String() string {
	if imp.Alias {
		return fmt.Sprintf("%s %q", imp.Name, imp.Path)
	}
	return fmt.Sprintf("%q", imp.Path)
}

// Type 类型标识, <package>.<name>
type Type struct {
	Path  string `json:"path"`  //类型所在包
	Sel   string `json:"sel"`   //类型选择前缀
	Name  string `json:"name"`  //类型名称
	Star  bool   `json:"star"`  //加星
	Len   string `json:"array"` //集合： 集合长度
	Key   *Type  `json:"key"`   //表: 表键类型
	Value *Type  `json:"value"` //表、星: 值类型
}

func (t *Type) Elem() *Type {
	if t.Star && t.Value != nil {
		return t.Value.Elem()
	}
	return t
}

func (t *Type) Go() string {
	if t == nil {
		return "interface{}"
	}
	s := strings.Builder{}
	if t.Star {
		s.WriteByte('*')
	}

	switch t.Path + "." + t.Name {
	case ".map":
		s.WriteString("map[")
		s.WriteString(t.Key.String())
		s.WriteByte(']')
		s.WriteString(t.Value.String())
	case ".slice":
		s.WriteString("[]")
		s.WriteString(t.Value.String())
	case ".array":
		s.WriteByte('[')
		s.WriteString(t.Len)
		s.WriteByte(']')
		s.WriteString(t.Value.String())
	default:
		if t.Sel != "" {
			s.WriteString(t.Sel)
			s.WriteByte('.')
		}
		s.WriteString(t.Name)
	}

	return s.String()
}

func (t *Type) String() string {
	if t == nil {
		return "interface{}"
	}
	s := strings.Builder{}
	if t.Star {
		s.WriteByte('*')
	}

	switch t.Path + "." + t.Name {
	case ".map":
		s.WriteString("map[")
		s.WriteString(t.Key.String())
		s.WriteByte(']')
		s.WriteString(t.Value.String())
	case ".slice":
		s.WriteString("[]")
		s.WriteString(t.Value.String())
	case ".array":
		s.WriteByte('[')
		s.WriteString(t.Len)
		s.WriteByte(']')
		s.WriteString(t.Value.String())
	default:
		if t.Path != "" {
			s.WriteString(t.Path)
			s.WriteByte('.')
		}
		s.WriteString(t.Name)
	}

	return s.String()
}

//Struct 消息
type Struct struct {
	Type   *Type    `json:"type"`   //结构体类型
	Fields []*Field `json:"fields"` //结构体字段
}

// Field 消息字段
type Field struct {
	Name    string `json:"name"`    //字段名
	Type    *Type  `json:"type"`    //字段类型
	Bind    string `json:"bind"`    //字段绑定 //[]*FieldBind
	Comment string `json:"comment"` //注释
}

func (f *Field) String() string {
	return fmt.Sprintf("%s %s %s %s", f.Name, f.Type, f.Bind, f.Comment)
}

// Func 方法
type Func struct {
	Path    string   `json:"path"`    //包名
	Sel     string   `json:"sel"`     //选择器
	Name    string   `json:"name"`    //名
	Router  string   `json:"router"`  //路由
	Params  []*Param `json:"params"`  //输入参数
	Results []*Param `json:"results"` //输出参数
	Method  []string `json:"method"`  //请求方法
	Comment []string `json:"comment"` //注释
}

func (f *Func) Package() string {
	return f.Path + "." + f.Name
}

func (f *Func) ID() string {
	return snakecase.Snakecase(f.Package())
}

func (f *Func) Go() string {
	return f.Sel + "." + f.Name
}

func (f *Func) ResultIDs() string {
	s := make([]string, 0, len(f.Results))
	for _, param := range f.Results {
		s = append(s, param.Name)
	}
	return strings.Join(s, ", ")
}

func (f *Func) ParamIDs() string {
	s := make([]string, 0, len(f.Params))
	for _, param := range f.Params {
		s = append(s, param.Name)
	}
	return strings.Join(s, ", ")
}

// Param 参数
type Param struct {
	Index int    `json:"index"` //索引位置
	Name  string `json:"name"`  //参数名
	Type  *Type  `json:"type"`  //参数类型
}

func (p *Param) NoName() bool {
	return p == nil || p.Name == "" || p.Name == "_"
}

func (p *Param) Context() bool {
	return p.Type != nil && p.Type.Path == "context" && p.Type.Name == "Context"
}

func (p *Param) Err() bool {
	return p.Type != nil && p.Type.Path == "" && p.Type.Name == "error"
}

// Project 项目
type Project struct {
	Root string `json:"root"` //跟目录
	Mod  string `json:"mod"`  //模块名

	Imports *Imports  `json:"imports"` //导入
	Structs []*Struct `json:"structs"` //消息
	Funs    []*Func   `json:"funcs"`   //方法

	//Name       string
	//Version    string
	//GitVersion string
	//BuildTime  string
}

func Parse(srcMod *mod.File) (prj *Project, err error) {
	builtins := getBuiltinType()
	_ = builtins
	prj = &Project{
		Root:    filepath.Dir(srcMod.Path()),
		Mod:     srcMod.Package(),
		Imports: &Imports{},
	}

	var files []*File

	err = filepath.WalkDir(prj.Root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		name := d.Name()
		if d.IsDir() {
			if len(name) == 0 || name[0] == '.' || name[0] == '_' {
				return filepath.SkipDir
			}
			return nil
		}
		if !strings.HasSuffix(name, ".api.go") && !strings.HasSuffix(name, ".model.go") {
			return nil
		}
		f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
		if err != nil {
			return errors.Wrap(err)
		}

		file := &File{path: path, File: f, root: prj.Root, mod: prj.Mod}
		if dir := filepath.Join(filepath.Dir(filepath.Dir(file.path)), getIdent(file.Name)); strings.HasPrefix(dir, file.root) {
			file.rel = filepath.ToSlash(strings.TrimPrefix(dir, file.root))
		}
		file.pkg = file.mod + file.rel

		files = append(files, file)
		return nil
	})

	prj.Imports.Find("gitee.com/go-wena/app")
	prj.Imports.Find("context")
	prj.Imports.Find("log")
	prj.Imports.Find("time")

	for _, file := range files {
		file.parseImport()
		prj.parseFuncs(file)
	}

	for _, file := range files {
		prj.parseStructs(file)
	}

	return prj, err
}

func (prj *Project) parseFuncs(file *File) {
	for _, decl := range file.Decls {
		if funcDecl, ok := decl.(*ast.FuncDecl); ok {
			if isExportIdent(funcDecl.Name) {
				prj.Funs = append(prj.Funs, file.parseFunc(funcDecl, prj.Imports))
			}
		}
	}
}

func (prj *Project) parseStructs(file *File) {
	for _, decl := range file.Decls {
		if genDecl, ok := decl.(*ast.GenDecl); ok {
			for _, spec := range genDecl.Specs {
				if ts, ok := spec.(*ast.TypeSpec); ok {
					if st, ok := ts.Type.(*ast.StructType); ok {
						prj.Structs = append(prj.Structs, file.parseStruct(st, getIdent(ts.Name)))
					}
				}
			}
		}
	}
}

type File struct {
	root string
	mod  string
	path string
	pkg  string
	rel  string
	fis  map[string]string
	*ast.File
}

func (file *File) parseImport() {
	file.fis = make(map[string]string, len(file.Imports))
	for _, spec := range file.Imports {
		var (
			name = getIdent(spec.Name)
			path = strings.Trim(spec.Path.Value, `"`)
		)
		if name != "" {
			file.fis[name] = path
		} else {
			name = mod.GetNameFromPath(path)
			if _, find := file.fis[name]; !find {
				file.fis[name] = path
			}
		}
	}
	return
}

func (file *File) parseStruct(st *ast.StructType, name string) *Struct {
	s := &Struct{Type: &Type{Path: file.pkg, Name: name}}
	for _, astField := range st.Fields.List {
		field := file.parseField(astField)
		if isExport(field.Name) {
			s.Fields = append(s.Fields, field)
		}
	}
	return s
}

func (file *File) parseField(field *ast.Field) *Field {
	return &Field{
		Name:    getIdents(field.Names),
		Type:    file.getFieldType(field.Type, nil),
		Bind:    getBasicValue(field.Tag),
		Comment: getGroupComment(field.Comment),
	}
}

func (file *File) parseFunc(funcDecl *ast.FuncDecl, imports *Imports) *Func {
	f := &Func{Name: getIdent(funcDecl.Name), Path: file.pkg, Sel: imports.Find(file.pkg)}

	f.Router, f.Method, f.Comment = parseFuncComment(funcDecl.Doc)
	for i, param := range funcDecl.Type.Params.List {
		p := &Param{Index: i, Name: getIdents(param.Names), Type: file.getFieldType(param.Type, imports)}
		p.Name = "i" + strconv.Itoa(i+1)
		f.Params = append(f.Params, p)
	}

	if f.Router == "" {
		f.Router = "/" + strings.Trim(f.Path[len(file.mod):]+"."+snakecase.Snakecase(f.Name), "./ ")
	}

	if len(f.Method) == 0 {
		f.Method = []string{http.MethodPost}
	}

	if funcDecl.Type.Results != nil {
		for i, param := range funcDecl.Type.Results.List {
			p := &Param{Index: i, Name: getIdents(param.Names), Type: file.getFieldType(param.Type, imports)}
			p.Name = "r" + strconv.Itoa(i+1)
			f.Results = append(f.Results, p)
		}
	}

	return f
}

func (file *File) getFieldType(expr ast.Expr, imports *Imports) *Type {
	switch typ := expr.(type) {
	case *ast.SelectorExpr:
		if ident, isIdent := typ.X.(*ast.Ident); isIdent {
			path := getIdent(ident)
			sel := path
			if pkg, find := file.fis[path]; find {
				if path = pkg; imports != nil {
					sel = imports.Find(path)
				}
			}
			return &Type{Name: getIdent(typ.Sel), Path: path, Sel: sel}
		}
		log.Fatalf("unknown selector: %T", typ.X)
	case *ast.StarExpr:
		t := file.getFieldType(typ.X, imports)
		t.Star = true
		return t
	case *ast.Ident:
		name := getIdent(typ)
		if isBuiltin(name) {
			return &Type{Name: name}
		}
		sel := ""
		if imports != nil {
			sel = imports.Find(file.pkg)
		}
		return &Type{Name: name, Path: file.pkg, Sel: sel}
	case *ast.StructType:
		return &Type{Name: "struct"}
	case *ast.ArrayType:
		if typ.Len == nil {
			return &Type{Name: "slice", Value: file.getFieldType(typ.Elt, imports)}
		}
		l, _ := typ.Len.(*ast.BasicLit)
		return &Type{Name: "array", Value: file.getFieldType(typ.Elt, imports), Len: getBasicValue(l)}
	case *ast.MapType:
		return &Type{Name: "map", Key: file.getFieldType(typ.Key, imports), Value: file.getFieldType(typ.Value, imports)}
	default:
		log.Fatalf("unknown type:<%T: %v>", typ, typ)
	}
	return nil
}

func parseFuncComment(doc *ast.CommentGroup) (router string, methods []string, comments []string) {
	if doc != nil && len(doc.List) > 0 {
		routerMatch := regexp.MustCompile(`@router(\s+(?P<path>/[a-zA-Z_\-/]+))?(\s+\[(?P<method>[a-zA-Z,\s]+)])?`)
		for _, cmt := range doc.List {
			text := strings.TrimLeft(cmt.Text, "/ ")
			if strings.HasPrefix(text, "@router ") {
				if match := findRegexpMatch(text, routerMatch); len(match) > 0 {
					router = match["path"]
					methods = strings.Fields(strings.ToUpper(match["method"]))
					methods = split(strings.ToUpper(match["method"]), ",")
					continue
				}
			}
			comments = append(comments, strings.TrimLeft(cmt.Text, "/ "))
		}
	}
	if len(methods) == 0 {
		methods = []string{"POST"}
	}
	return
}

func findRegexpMatch(s string, exp *regexp.Regexp) (result map[string]string) {
	match := exp.FindStringSubmatch(s)
	if len(match) > 0 {
		groupNames := exp.SubexpNames()
		result = make(map[string]string, len(match))
		for i, name := range groupNames {
			if i > 0 {
				if name != "" {
					result[name] = match[i]
				}
			}
		}
	}
	return result
}

func split(s string, spt string) (list []string) {
	ss := strings.Split(strings.TrimSpace(s), spt)
	for _, si := range ss {
		if si = strings.TrimSpace(si); si != "" {
			list = append(list, si)
		}
	}
	return
}

func getBasicValue(b *ast.BasicLit) string {
	if b != nil {
		return b.Value
	}
	return ""
}

func getIdents(vs []*ast.Ident) string {
	return getIdent(vs...)
}

func getIdent(vs ...*ast.Ident) string {
	switch len(vs) {
	case 0:
		return ""
	case 1:
		if vs[0] != nil {
			return vs[0].Name
		}
		return ""
	default:
		s := strings.Builder{}
		for _, v := range vs {
			if v != nil && v.Name != "" {
				if s.Len() > 0 {
					s.WriteByte('.')
				}
				s.WriteString(v.Name)
			}
		}
		return s.String()
	}
}

func isExport(name string) bool {
	return len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z'
}

func isExportIdent(vs ...*ast.Ident) bool {
	name := getIdents(vs)
	return len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z'
}

func getGroupComment(c *ast.CommentGroup) string {
	if c != nil {
		return "// " + strings.TrimSpace(c.Text())
	}
	return ""
}

func isBuiltin(typeName string) bool {
	if builtinTypeMap == nil {
		builtinTypeMap = getBuiltinType()
	}
	return builtinTypeMap[typeName]
}

var builtinTypeMap map[string]bool

func getBuiltinType() (builtinTypeMap map[string]bool) {
	return map[string]bool{
		"rune":       true,
		"bool":       true,
		"uint64":     true,
		"int16":      true,
		"float64":    true,
		"complex64":  true,
		"uint":       true,
		"byte":       true,
		"uint32":     true,
		"int8":       true,
		"int32":      true,
		"float32":    true,
		"complex128": true,
		"int":        true,
		"uint8":      true,
		"uint16":     true,
		"int64":      true,
		"string":     true,
		"uintptr":    true,
		"error":      true,
	}

	//builtinTypeMap = map[string]bool{}
	//builtinFn := filepath.Join(runtime.GOROOT(), "src/builtin/builtin.go")
	//file, err := parser.ParseFile(token.NewFileSet(), builtinFn, nil, 0)
	//if err != nil {
	//	return
	//}
	//for _, decl := range file.Decls {
	//	if gen, ok := decl.(*ast.GenDecl); ok {
	//		for _, spec := range gen.Specs {
	//			if ts, ok := spec.(*ast.TypeSpec); ok {
	//				name := getIdent(ts.Name)
	//				if !isExport(name) {
	//					builtinTypeMap[name] = true
	//				}
	//			}
	//		}
	//	}
	//}
	//
	//return
}

type Imports struct {
	selPackage map[string]map[string]string
	Imports    []*Import `json:"imports"`
}

func (imp *Imports) Find(path string) (name string) {
	var alias bool
	name = mod.GetNameFromPath(path)

	if len(imp.selPackage) == 0 {
		imp.selPackage = map[string]map[string]string{name: {path: name}}
	} else if packages, find := imp.selPackage[name]; find {
		if name, find = packages[path]; find {
			return
		}
		if i := len(packages) - 1; i > 0 {
			name = name + strconv.Itoa(i)
			alias = true
		}
		imp.selPackage[name][path] = name
	} else {
		imp.selPackage[name] = map[string]string{path: name}
	}

	imp.Imports = append(imp.Imports, &Import{Name: name, Path: path, Alias: alias})
	sort.SliceStable(imp.Imports, func(i, j int) bool { return imp.Imports[i].Path < imp.Imports[j].Path })
	return
}
